////////////////////////////////////////////////////////////////////////////////
//
//	Original file Name: GameFrame.java
//	Originating Author: JWhitefield
//	
//	Description: Implementation of a game controller input device
//
////////////////////////////////////////////////////////////////////////////////

package com.jjw.game.input;

import net.java.games.input.Component;
import net.java.games.input.Component.POV;
import net.java.games.input.Controller;
import net.java.games.input.Controller.Type;
import net.java.games.input.Rumbler;

/**
 * Implementation of a game controller input device
 * @author JWhitefield
 *
 */
public class InputDeviceGamePad extends InputDevice {

	
	//--------------------------------------------------------------------------
	//
	//Properties
	//
	//--------------------------------------------------------------------------
	
	//----------------------------------
	// Public
	//----------------------------------
	
	//----------------------------------
	//  Getters / Setters
	//----------------------------------
	
	//----------------------------------
	// Private
	//----------------------------------
		
	/**
	 * The compass positions available for the directional pads 
	 */
	private static final int NW = 0;
	private static final int NORTH = 1;
	private static final int NE = 2;
	private static final int WEST = 3;
	private static final int NONE = 4;
	private static final int EAST = 5;
	private static final int SW = 6;
	private static final int SOUTH = 7;
	private static final int SE = 8;
	
	/**
	 * Total numbs of buttons the controller has 
	 */
	private static final int NUM_BUTTONS = 12;
	
	/**
	 * The game pad controller we will be using 
	 */
	private Controller gamepad;
	
	/**
	 * The individual components the game pad is made up from 
	 */
	private Component[] components;
	
	/**
	 * The rumbler's for this controller
	 */
	private Rumbler[] rumblers;
	
	/**
	 * xAxis component id
	 */
	private int xAxisId;
	
	/**
	 * yAxis component id
	 */
	private int yAxisId;
	
	/**
	 * zAxis component id
	 */
	private int zAxisId;
	
	/**
	 * rzAxis component Id 
	 */
	private int rzAxisId;
	
	/**
	 * point of view component Id
	 */
	private int povId;
	
	/**
	 * Id's for the buttons
	 */
	private int buttonsId[];
	
	/**
	 * Id for the rumbler component being used 
	 */
	private int rumberId;
	
	/**
	 * Flag showing if the rumbler is currently on or not
	 */
	private boolean rumblerOn = false;
	
	//--------------------------------------------------------------------------
	//
	//  Methods
	//
	//--------------------------------------------------------------------------
	
	//----------------------------------
	// Public
	//----------------------------------
	
	InputDeviceGamePad( Controller gamepad ) {

		//Sets up the game pad with the controller passed in
		this.gamePadSetController( gamepad );
	}
	
	/**
	 * Sets the game pad using the controller passed in 
	 * 
	 * @param gamepad 	the controller object that will be used for this game pad
	 * 
	 * @throws ExceptionInInitializerError 	when controller passed in is not of the correct type 
	 */
	void gamePadSetController( Controller gamepad )
	{
		//Check the controller isn't null 
		if( gamepad == null )
		{
			System.err.println("GamePad: Cannot create a GamePad controller with null controller ");
			return;
		}
		 
		//Check the controller passed in is of the correct type, if not thow an exception 
		if( gamepad.getType() != Type.GAMEPAD && gamepad.getType() != Type.STICK )
		{
			System.err.println("GamePad: Cannot create a GamePad with an incompattible controller type "+ gamepad.getType().toString() );
			return;
		}
			
		//Set the game pad
		this.gamepad = gamepad;
		
		//Finds the indexes for the controllers components 
		this.findComponentIndexs();
	}
	
	/**
	 * Sets the rumbler to the state passed in 
	 * 
	 * @param rumbleOn	set the rumble on if true, off if false
	 * @param duration  set rumble on is true and duration is set duration of rumble is set
	 */
	void setRumbler( boolean rumbleOn, int duration ){
		
		//Check that we have an id for the rumbler
		if( this.rumberId != -1 ){
		
			//Decide weather we are turning them on or off depending on the flag
			if( rumbleOn ){
				//If we are turning them on set the rumble value to 0.8
				this.rumblers[ this.rumberId ].rumble(0.8f);
			}else{
				//If we are turning them off set the rumble value to 0
				this.rumblers[ this.rumberId ].rumble(0.0f);
			}
			
			//Set the rumble state
			this.rumblerOn = rumbleOn;
		}
	}
	
	/**
	 * Sets the rumbler state
	 * 
	 * @param rumbleOn	the state of the rumbler
	 */
	void setRumbler( boolean rumbleOn ){
		this.setRumbler(rumbleOn, 0);
	}	
	
	/**
	 * Prints the current state of the game pad to the console
	 */
	void printGamePadState(){
		
		//Poll the game pad so its value update
		this.gamepad.poll();
		
		//Prints out the current controller sticks and pov hat
		System.out.print("Game Pad: (X/Y) Stick: "+this.getXYStickCompassPosition());
		System.out.print(" (Z/RZ) Stick: "+this.getZRZStickCompassPosition());
		System.out.print(" POV Hat: "+this.getPOVHatCompassPosition());
		
		//Prints out the buttons states
		System.out.print(" Buttons ");
		boolean[] buttonStates = this.getButtons();
		for( int i = 0; i < buttonStates.length; i++)
			System.out.print( " "+(i+1)+": "+buttonStates[i] );
		
		//Print out the buttons rumble state
		System.out.print(" Rumbling: "+this.rumblerOn);
		
		//Print a blank line so that its neat 
		System.out.println("");
	}
	
	//----------------------------------
	// Private
	//----------------------------------
	
	/**
	 * Function finds the indexes for each component of the controller 
	 */
	private void findComponentIndexs(){
		
		//Get the components from the game pad 
		this.components = this.gamepad.getComponents();
		
		//Check that the controller has some components 
		if( this.components.length == 0 ){
			
			System.err.println("GamePad: Cannot setup a controller which has no components");
			return;
		}
		
		//Get the index for the xAxis Stick 
		this.xAxisId = this.findComponentIndex( Component.Identifier.Axis.X );
		
		//Get the index for the yAxis Stick 
		this.yAxisId = this.findComponentIndex( Component.Identifier.Axis.Y );
		
		//Get the index for the zAxis Stick
		this.zAxisId = this.findComponentIndex( Component.Identifier.Axis.Z );
		
		//Get the index for the rzAxis Stick
		this.rzAxisId = this.findComponentIndex( Component.Identifier.Axis.RX );
		
		//Get the index for the Point of View hat
		this.povId = this.findComponentIndex( Component.Identifier.Axis.POV );
		
		//Find the buttons 
		this.findButtons();
		
		//Find the rumblers
		this.findRumblers();
	}
	
	/**
	 * Functions finds the ids of the buttons on the game pad
	 */
	private void findButtons(){
		
		//Create a new array for the buttons indexes 
		this.buttonsId = new int[ NUM_BUTTONS ];
		
		//Placeholder button component 
		Component buttonComponent;
		
		//Count on the number of buttons we have found 
		int numButtons = 0;
		
		//Loop through the components list looking for the buttons list 
		for( int i = 0; i < this.components.length; i++ ){
			
			//Get the component at this index 
			buttonComponent = this.components[i];
			
			//Check if this component is a button 
			if( this.isButton( buttonComponent ) ){
				
				//Check that we havn't filled our button quota
				if( numButtons != NUM_BUTTONS ){
					
					//Store this buttons index 
					this.buttonsId[numButtons] = i;
					
					//Up the number of buttons we have found
					numButtons++;
				}
				else{
					System.err.println("GamePad: Already filled up or button allocation");
					return;
				}
			}
		}
		
		//If we have not filled our button indexes we will fill them with -1 
		if( numButtons < NUM_BUTTONS ){
			
			//Repeat through the while loop until the array if full
			while( numButtons < NUM_BUTTONS ){
				
				//Add -1 to the button index array
				this.buttonsId[numButtons] = -1;
				
				//Up the number of buttons
				numButtons++;
			}
		}
	}
	
	/**
	 * Functions which finds the rumbler's for the game pad
	 */
	private void findRumblers(){
		
		//Get the rumbler's for this game pad
		this.rumblers = this.gamepad.getRumblers();
		
		//Check if the game pad has any rumbler's 
		if( this.rumblers.length == 0 ){
			
			//If no rumbler's were found then set the rumbler's index to -1
			this.rumberId = -1;
		}else{
			
			//If it has rumbler's then use the last one on the list
			this.rumberId = this.rumblers.length-1;			
		}	
	}
	
	/**
	 * Function which checks if the component passed in is a button or not 
	 * 
	 * @param possibleButton	Component we are checking to see if its a button or not
	 * @return					true if it is a button false if not
	 */
	private boolean isButton( Component possibleButton ){
		
		//Check that the component is both digital and absolute e.g. 1 or 0 no mid point 
		if( !possibleButton.isAnalog() && !possibleButton.isRelative() ){
			
			//Get the name of the identifier class that this component bellongs to 
			String componentClassName = possibleButton.getIdentifier().getClass().getName();
			
			//If the end of the class name contains 'Button' then we know its a button
			if( componentClassName.endsWith("Button") )
				return true;
		}
		
		//If we get here then its not a button so return false
		return false;
	}
	
	/**
	 * Finds the index of a specific component passed in 
	 */
	private int findComponentIndex( Component.Identifier componentId ){
		
		//Component we will use as a placeholder in the look whilst looking the the component index
		Component searchComponent;
		
		//Loop through the components looking at each one to see if its the one we want 
		for( int i = 0; i < this.components.length; i++ ){
			
			//Set the placeholder component to make it easier to check 
			searchComponent = this.components[i];
			
			//Check the components identifier and that it is not a relative component 
			if( searchComponent.getIdentifier() == componentId && !searchComponent.isRelative() )
				return i;
		}
		
		//If we couldn't find the component return -1
		return -1;
	}
	
	/**
	 * Get the compass position of the x/y stick
	 *  
	 * @return a compass direction constant 
	 */
	private int getXYStickCompassPosition(){
		
		//If either of the x or y stick component id's havn't been set, we can't get the position so 
		// return the default NONE position 
		if( this.xAxisId == -1 || this.yAxisId == -1 ){
			return NONE;
		}
		
		//Get the compass direction
		return this.getCompassDirection( this.xAxisId, this.yAxisId);
	}
	
	/**
	 * Get the compass position of the z/rz stick
	 *  
	 * @return a compass direction constant 
	 */
	private int getZRZStickCompassPosition(){
		
		//If either of the z or rz stick component id's havn't been set, we can't get the position so 
		// return the default NONE position 
		if( this.rzAxisId == -1 || this.zAxisId == -1 ){
			return NONE;
		}
		
		//Get the compass direction
		return this.getCompassDirection( this.zAxisId, this.rzAxisId);
	}
	
	/**
	 * Get the compass position of the POV Hat
	 * 
	 * @return a compass direction constant
	 */
	private int getPOVHatCompassPosition(){
		
		//Check that the if for the POV hat has been set
		if( this.povId == -1 ){
			//If it isn't set we will return no position 
			return NONE;
		}
			
		//Get the position of the pov hat
		float povHatPosition = this.components[ this.povId ].getPollData();
		
		//Compare the POV hat position to the constants to find its position
		if (povHatPosition == POV.CENTER)
			return NONE;
		else if (povHatPosition == POV.DOWN)
			return SOUTH;
		else if (povHatPosition == POV.DOWN_LEFT)
			return SW;
		else if (povHatPosition == POV.DOWN_RIGHT)
			return SE;
		else if (povHatPosition == POV.LEFT)
			return WEST;
		else if (povHatPosition == POV.RIGHT)
			return EAST;
		else if (povHatPosition == POV.UP)
			return NORTH;
		else if (povHatPosition == POV.UP_LEFT)
			return NW;
		
		//If we are yet to return a compass direction, we will assume its in the center
		return NONE;
	}
	
	/**
	 * Get the buttons pressed state
	 * 
	 * @return an array of the current buttons pressed state
	 */
	private boolean[] getButtons(){
		
		//Create an array of booleans which will show the buttons states
		boolean[] buttonStates = new boolean[ NUM_BUTTONS ];
		
		//The value of the button we are looking at pressed state 
		float buttonValue;
		
		//Loop through all the buttons so we can represent their pushed state
		for( int i = 0; i < NUM_BUTTONS; i++ ){
			
			//Get the pressed value for that button
			buttonValue = this.components[ this.buttonsId[ i ] ].getPollData();
			
			//If the button value if zero its not pressed, if it it is
			buttonStates[ i ] = ( buttonValue == 0.0f )? false:true;
		}
		
		//Return the button states
		return buttonStates;
	}
		
	/**
	 * Finds the compass directions for the component id's passed integers
	 * 
	 * @param xAxisId 	the id of the xAxis component which we are getting the compass direction for 	
	 * @param yAxisId	the id of the yAxis component which we are getting the compass direction for
	 * @return			the id of the compass directions the components are in  
	 */
	private int getCompassDirection( int xa, int ya ){
		
		//Get the rounded x coordinate 
		int xCoord = Math.round( this.components[ xa ].getPollData() );

		//Get the rounded y coordinate 
		int yCoord = Math.round( this.components[ ya ].getPollData() );
		
		//Check the state of both the z and y coord's and work out the compass
		//position of the controller 
		if ((yCoord == -1) && (xCoord == -1))
			return NW;
		else if ((yCoord == -1) && (xCoord == 0))
			return NORTH;
		else if ((yCoord == -1) && (xCoord == 1))
			return NE;
		else if ((yCoord == 0) && (xCoord == -1))
			return WEST;
		else if ((yCoord == 0) && (xCoord == 0))
			return NONE;
		else if ((yCoord == 0) && (xCoord == 1))
			return EAST;
		else if ((yCoord == 1) && (xCoord == -1))
			return SW;
		else if ((yCoord == 1) && (xCoord == 0))
			return SOUTH;
		else if ((yCoord == 1) && (xCoord == 1))
			return SE;
		
		//If we reach here and we havn't found the coordinates then we must be in no position 
		return NONE;	
	}

	/**
	 * Read the controller inputs and process then into meaningful input buttons
	 */
	private void readControllerInputs() {
		
		//Check that we have a game pad
		if( this.gamepad == null )
		{
			System.err.println("GAMEPAD: Cannot read input of a non existant gamepad");
			return;
		}
		
		//Poll the controller to read its updated inputs
		this.gamepad.poll();
		
		//Get the direction pad states
		int povPosition = this.getPOVHatCompassPosition();
		int xyPosition = this.getXYStickCompassPosition();
		int zrzPosition = this.getZRZStickCompassPosition();

		//Get the input values in the array
		int[] inputValues =  {povPosition, xyPosition, zrzPosition};
		
		//Check for the up button
		if( this.checkInputAgainstResult( inputValues, NORTH, NE, NW ))
			this.getUpInputButton().toggle(true);
		else 
			this.getUpInputButton().toggle(false);
		
		//Check for the down button
		if( this.checkInputAgainstResult( inputValues, SOUTH, SE, SW ))
			this.getDownInputButton().toggle(true);
		else 
			this.getDownInputButton().toggle(false);
		
		//Check for the left button
		if( this.checkInputAgainstResult( inputValues, WEST, SW, NW ))
			this.getLeftInputButton().toggle(true);
		else 
			this.getLeftInputButton().toggle(false);
		
		//Check for the right button
		if( this.checkInputAgainstResult( inputValues, EAST, NE, SE ))
			this.getRightInputButton().toggle(true);
		else 
			this.getRightInputButton().toggle(false);
	}
	
	/**
	 * Check a list of input values against results values
	 * 
	 * @param input
	 * @param result
	 * @return
	 */
	private boolean checkInputAgainstResult( int[] input, int dir1, int dir2, int dir3 ) {
		
		//Loop through both lists and if any of the inputs are equal to any of the results return true
		for( int inputValue: input )
			if( inputValue == dir1 || inputValue == dir2 || inputValue == dir3 )
				return true;
		
		//If not return false
		return false;
	}
	
	//----------------------------------
	// Overridden / Implemented
	//----------------------------------
	
	@Override
	public void tick() {
		
		//Call the super tick
		super.tick();
		
		//Read the controller inputs
		this.readControllerInputs();
	}
}